Decorator Pattern 2

Closure를 활용한 decorator 매개변수화(decorator factory)

예시

구현목표

- 받아들일 수 있는 위치기반 인자의 자료형과 개수 지정
- 그러한 특성을 함수마다 다르게 적용
# @statically_typed: 함수나 메서드, 클래스를 유일한 인자로 받기때문에 decorator는 아님.
@statically_typed(str, str, return_type=str)  # decorator factory
def make_tagged(text, tag):
    return "<{0}{1}</{0}>".format(tag, escape(text))

@statically_typed(str, int, str)  # 어떤 반환 타입이든 받아들일 수 있음.
def repeat(what, count, separator):
    return ((what + separator) * count)[:-len(separator)]

In [9]:
def statically_typed(*types, return_type=None):  # Decorator factory
    def decorator(function):  # 팩토리가 반환할 decorator 함수
        @functools.wraps(function)
        def wrapper(*args, **kwargs):  # Decorator가 반환할 래퍼 함수
            if len(args) > len(types):
                raise ValueError("too many arguments")
            elif len(args) < len(types):
                raise ValueError("too few arguments")
                
            for i, (arg, type_) in enumerate(zip(args, types)):
                # 자료값의 Type 비교(지정인자, 지정타입 일치여부)
                if not isinstance(arg, type_):
                    raise ValueError("argument {} must be of type {}"
                                     .format(i, type_.__name__))
            result = function(*args, **kwargs)
            
            if (return_type is not None and
                not isinstance(result, return_type)):  # result의 Type 비교
                raise ValueError("return value must be of type {}".format(
                    return_type.__name__))
            return result
        return wrapper
    return decorator

In [10]:
@statically_typed(str, str, return_type=str)
def make_tagged(text, tag):
    return "<{0}{1}</{0}>".format(tag, escape(text))

@statically_typed(str, int, str)
def repeat(what, count, separator):
    return ((what + separator) * count)[:-len(separator)]

In [13]:
repeat("ABC", "3", ",")


---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-13-7b3da1ade2c0> in <module>()
----> 1 repeat("ABC", "3", ",")

<ipython-input-9-6eee9841102c> in wrapper(*args, **kwargs)
     12                 if not isinstance(arg, type_):
     13                     raise ValueError("argument {} must be of type {}"
---> 14                                      .format(i, type_.__name__))
     15             result = function(*args, **kwargs)
     16 

ValueError: argument 1 must be of type int

In [12]:
args = ["ABC", 3, ","]
types = [str, int, str]
for i, (arg, type_) in enumerate(zip(args, types)):
    print(i, (arg, type_))


0 ('ABC', <class 'str'>)
1 (3, <class 'int'>)
2 (',', <class 'str'>)

내부 동작순서 확인

def statically_typed(*types, return_type=None):  # 1. statically_typed 호출
    def decorator(function):  # 3. function 호출(유일한 인자로 decorator에 전달)
        @functools.wraps(function)
        def wrapper(*args, **kwargs):
            return result
        return wrapper  # 4. 저장된 자료형에 따른(가변적) 새로운 wrapper() 반환/진입
    return decorator  # 2. decorator 반환/진입(statically_typed 인자 저장, closure)

매개변수화되지 않은 decorator 예시

구현목표

  • 사용자가 로그인해 있을 때에만 페이지에 접근허용, 아니면 login 페이지로 이동
@application.post("/mailinglists/add")  # 최종적으로 mailinglist/add 페이지에 접근
@Web.ensure_logged_in  # 페이지 접근 전 로그인 확인
def person_add_submit(username):
    name = bottle.request.forms.get("name")
    try:
        id = Data.MailingLists.add(name)
        bottle.redirect("/mailinglists/view")
    except Data.Sql.Error as err:
        return bottle.mako_template("error", url="/mailinglists/add",
                                    text="Add Mailinglist", message=str(err))

def ensure_logged_in(function):
    @functools.wraps(function)
    def wrapper(*args, **kwargs):
        # 사용자 계정명 조회
        username = bottle.request.get_cookie(COOKIE, secret=secret(bottle.request))
        # 사용자 로그인 상태 확인
        if username is not None:
            # 키워드 인자에 사용자 계정명 추가
            kwargs["username"] = username
            # 원본 함수 반환
            return function(*args, **kwargs)
        bottle.redirect("/login")
    return wrapper

Class decorator

  • getter와 setter

In [14]:
class Book:
    def __init__(self):
        self._price = 1000
    
    def get_price(self):
        print("Getter gets...")
        return self._price
    def set_price(self, value):
        if type(value) == int:
            print("Setter sets...", value)
            self._price = value
        else:
            raise ValueError("Value must be integer")

book = Book()
print(book.get_price())
book.set_price(100)
print(book.get_price())
book.set_price("100원")


Getter gets...
1000
Setter sets... 100
Getter gets...
100
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-14-fe8acfc9e9c9> in <module>()
     17 book.set_price(100)
     18 print(book.get_price())
---> 19 book.set_price("100원")

<ipython-input-14-fe8acfc9e9c9> in set_price(self, value)
     11             self._price = value
     12         else:
---> 13             raise ValueError("Value must be integer")
     14 
     15 book = Book()

ValueError: Value must be integer
  • Pythonic: decorator 활용
    • price = property(get_price, set_price)
property(fget=None, fset=None, fdel=None, doc=None)

# 비어있는 property 인스턴스 생성
price = property()
# fget 설정
price = price.getter(get_price)
# fset 설정
price = price.setter(set_price)

In [15]:
class Book:
    def __init__(self):
        self._price = 1000
    
    @property  # 멤버변수 접근 시: 클래스.메서드
    def price(self):
        return self._price
    
    @price.setter  # 멤버변수 변경 시: 메서드.setter
    def price(self, value):
        if type(value) == int:
            self._price = value
        else:
            raise ValueError("Value must be integer")

book = Book()
print(book.price)
book.price = 100
print(book.price)
book.price = "100원"


1000
100
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-15-bd06bb8e4300> in <module>()
     18 book.price = 100
     19 print(book.price)
---> 20 book.price = "100원"

<ipython-input-15-bd06bb8e4300> in price(self, value)
     12             self._price = value
     13         else:
---> 14             raise ValueError("Value must be integer")
     15 
     16 book = Book()

ValueError: Value must be integer

중복코드 제거에 decorator 활용

  • Book 클래스 속성값: title, isbn, price, quantity
  • 4개의 property, 4개의 setter 필요
  • Decorator 활용으로 중복 최소화
@ensure("title", is_non_empty_str)  # 5. 4까지의 결과값 전달, 최종적으로 4개 속성 추가
@ensure("isbn", is_valid_isbn)  # 4. 3까지의 결과값 전달
@ensure("price", is_in_range(1, 10000))  # 3. 2까지의 결과값 전달
@ensure("quantity", is_in_range(0, 1000000))  # 2. Book 클래스 객체가 인자로 전달(quantity 추가)
class Book:  # 1. Book 클래스 객체 생성
    def __init__(self, title, isbn, price, quantity):
        self.title = title
        self.isbn = isbn
        self.price = price
        self.quantity = quantity
    @property  # 읽기전용 property(setter 없음.)
    def value(self):
        return self.price * self.quantity
  • 검증 함수 정의

In [15]:
# Title 검증: 제목 공란여부 확인
def is_non_empty_str(name, value):
    if not isinstance(value, str):
        raise ValueError("{} must be of type str".format(name))
    if not bool(value):
        raise  ValueError("{} may not be empty".format(name))

In [16]:
import numbers

In [17]:
# 값의 제한범위 포함여부 및 입력값 숫자여부(numbers.Number) 확인
def is_in_range(minimum=None, maximum=None):
    assert minimum is not None or maximum is not None  # 디버그 모드에서만 실행
    def is_in_range(name, value):
        # 숫자여부 검증
        if not isinstance(value, numbers.Number):
            raise ValueError("{} must be a number".format(name))
        # 최소값 조건 검증
        if minimum is not None and value < minimum:
            raise ValueError("{} {} is too small".format(name, value))
        # 최대값 조건 검증
        if maximum is not None and value > maximum:
            raise ValueError("{} {} is too big".format(name, value))
    return is_in_range
  • ensure 함수 정의

In [18]:
def ensure(name, validate, doc=None):  # property 이름, 검증 함수, docstring 받음.
    def decorator(Class):  # Class를 유일한 인자로 받음.
        
        # self.title의 property값은 self.__title에 저장
        privateName = "__" + name  # 외부접근이 불가능한 이름 생성
        
        def getter(self):  # 저장된 속성값 반환함수 생성
            # getattr: 객체, 속성명을 인자로 받아 속성값 반환(없으면 오류발생)
            return getattr(self, privateName)
        
        def setter(self, value):
            # 전달값 검증
            validate(name, value)
            # setattr: 객체, 속성명, 값을 받아 객체에 값을 해당 속성으로 설정(없으면 새로생성)
            setattr(self, privateName, value)
            
        setattr(Class, name, property(getter, setter, doc=doc))
        return Class
    return decorator

Decorator로 property 추가

  • 여러개의 decorator를 겹쳐 사용하고 싶지 않을 때, 클래스 내부에 속성을 삽입하여 가독성 향상
@do_ensure  # 각 Ensure 인스턴스를 같은 이름의 property로 변경
class Book:
    title= Ensure(in_non_empty,str)  # 각 Ensure는 검증 함수 저장
    isbn = Ensure(is_valid_isbn)
    price = Ensure(is_in_range(1, 10000))
    quantity = Ensure(is_in_range(0, 1000000))

    def __init__(self, title, isbn, price, quantity):
        self.title = title
        self.isbn = isbn
        self.price = price
        self.quantity = quantity

    @property
    def value(self):
        return self.price * self.quantity
  • Ensure 클래스 정의

In [19]:
class Ensure:
    def __init__(self, validate, doc=None):
        self.validate = validate
        self.doc = doc
  • do_ensure 정의

In [20]:
def do_ensure(Class):
    def make_property(name, attribute):
        privateName = "__" + name
        def getter(self):
            return getattr(self, privateName)
        def setter(self, value):
            attribute.validate(name, value)
            setattr(self, privateName, value)
        return property(getter, setter, doc=attribute.doc)
    
    print(Class.__dict__)
    for name, attribute in Class.__dict__.items():
        if isinstance(attribute, Ensure):
            setattr(Class, name, make_property(name, attribute))
    print(Class.__dict__)
    return Class
  • Closure 미사용 시 바인딩 시점에 문제발생

In [29]:
def do_ensure(Class):
    print(Class.__dict__)
    for name, attribute in Class.__dict__.items():
        if isinstance(attribute, Ensure):
            privateName = "__" + name
            def getter(self):
                return getattr(self, privateName)
            def setter(self, value):
                attribute.validate(name, value)
                setattr(self, privateName, value)
            setattr(Class, name, property(getter, setter, doc=attribute.doc))
    print(Class.__dict__)
    return Class
  • 동작확인

In [21]:
@do_ensure
class Book:
    title = Ensure(is_non_empty_str)
    price = Ensure(is_in_range(1, 10000))
    quantity = Ensure(is_in_range(0, 1000000))
    
    def __init__(self, title, price, quantity):
        self.title = title
        self.price = price
        self.quantity = quantity
       
    @property
    def value(self):
        return self.price * self.quantity


{'__module__': '__main__', 'quantity': <__main__.Ensure object at 0x00000243BFD90FD0>, 'title': <__main__.Ensure object at 0x00000243BFD90B70>, '__init__': <function Book.__init__ at 0x00000243BFD5BAE8>, '__weakref__': <attribute '__weakref__' of 'Book' objects>, '__dict__': <attribute '__dict__' of 'Book' objects>, '__doc__': None, 'value': <property object at 0x00000243BFD7A228>, 'price': <__main__.Ensure object at 0x00000243BFD90EF0>}
{'__module__': '__main__', 'quantity': <property object at 0x00000243BFE090E8>, 'title': <property object at 0x00000243BFE09188>, '__init__': <function Book.__init__ at 0x00000243BFD5BAE8>, '__weakref__': <attribute '__weakref__' of 'Book' objects>, '__dict__': <attribute '__dict__' of 'Book' objects>, '__doc__': None, 'value': <property object at 0x00000243BFD7A228>, 'price': <property object at 0x00000243BFE09A98>}

In [36]:
book = Book("get out of my sight", 1000, 1)

In [24]:
book.__dict__


Out[24]:
{'__price': 1000, '__quantity': 1, '__title': 'get out of my sight'}

In [25]:
book.title = 123


---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-25-0e930ab31269> in <module>()
----> 1 book.title = 123

<ipython-input-21-928fff0793b3> in setter(self, value)
      5             return getattr(self, privateName)
      6         def setter(self, value):
----> 7             attribute.validate(name, value)
      8             setattr(self, privateName, value)
      9         return property(getter, setter, doc=attribute.doc)

<ipython-input-16-0c82f8b8b4b1> in is_non_empty_str(name, value)
      2 def is_non_empty_str(name, value):
      3     if not isinstance(value, str):
----> 4         raise ValueError("{} must be of type str".format(name))
      5     if not bool(value):
      6         raise  ValueError("{} may not be empty".format(name))

ValueError: title must be of type str

클래스 decorator를 상속 대신 활용

  • 상속 시 데이터와 메서드의 값을 변경하지 않을 경우

In [ ]:
class Mediated:

    def __init__(self):
        self.mediator = None


    def on_change(self):
        if self.mediator is not None:
            self.mediator.on_change(self)

In [ ]:
class Button(Mediated):

    def __init__(self, text=""):
        super().__init__()
        self.enabled = True
        self.text = text

    def click(self):
        if self.enabled:
            self.on_change()

    def __str__(self):
        return "Button({!r}) {}".format(self.text,
                "enabled" if self.enabled else "disabled")


class Text(Mediated):

    def __init__(self, text=""):
        super().__init__()
        self.__text = text
    
    @property
    def text(self):
        return self.__text

    @text.setter
    def text(self, text):
        if self.text != text:
            self.__text = text
            self.on_change()


    def __str__(self):
        return "Text({!r})".format(self.text)

In [ ]:
def mediated(Class):
    setattr(Class, "mediator", None)
    def on_change(self):
        if self.mediator is not None:
            self.mediator.on_change(self)
    setattr(Class, "on_change", on_change)
    return Class

In [ ]:
@mediated
class Button:

    def __init__(self, text=""):
        super().__init__()
        self.enabled = True
        self.text = text

    def click(self):
        if self.enabled:
            self.on_change()

    def __str__(self):
        return "Button({!r}) {}".format(self.text,
                "enabled" if self.enabled else "disabled")


@mediated
class Text:

    def __init__(self, text=""):
        super().__init__()
        self.__text = text

    @property
    def text(self):
        return self.__text

    @text.setter
    def text(self, text):
        if self.text != text:
            self.__text = text
            self.on_change()

    def __str__(self):
        return "Text({!r})".format(self.text)